package com.example.spider.provider.m3u8;

import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.thread.ThreadUtil;
import com.example.spider.event.listener.SpiderUrlsManager;
import com.example.spider.provider.m3u8.dto.M3u8MetaInfo;
import com.example.spider.provider.m3u8.task.M3u8MetaInfoParserTask;
import com.example.spider.starter.SpiderStarter;
import com.example.spider.task.SpiderTaskQueue;
import com.example.spider.task.limit.LimitStrategy;
import com.example.spider.task.limit.NoLimit;
import com.example.spider.util.PathUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.shoulder.core.concurrent.Threads;
import org.shoulder.core.context.AppInfo;
import org.shoulder.core.util.ArrayUtils;
import org.shoulder.core.util.JsonUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.unit.DataSize;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author lym
 */
@Slf4j
@Component
public class M3u8Starter implements SpiderStarter {

    // -----------
    /**
     * 把缺少url信息，无法重试的任务永久删除掉
     */
    private static final boolean deleteTaskCanNotRetry = false;
    // -----------
    /**
     * 把下载了很多词都失败，而且也没下载多少的任务永久删除掉
     */
    private static final boolean deleteTaskNoProcess = false;
    private static final int CONFIG_RECOVER_MAX = 2000;
    private static final long CONFIG_DELETE_THRESHOLD = DataSize.ofMegabytes(300).toMegabytes();
    // -----------
    /**
     * 把下载完毕的移走，方便统一查看合并后的视频
     */
    private static final boolean MOVE_FINISHED = true;
    private static final String CONFIG_RESULT_COLLECT_DIR = "E:\\vedio_book\\random\\";


    @Autowired
    private M3u8FileNameTemplate m3u8FileNameTemplate;

    @Autowired
    private SpiderUrlsManager spiderUrlsManager;


    /**
     * 启动时扫描，方便后续移动位置
     */
    private Map<String, File> finishedTaskIdToDirMap = new HashMap<>();


    @Override
    public void startSpiders() {
        scanLocalAndRecoverTask();

//        M3u8MetaInfoParserTask parseMetaInfoTask = new M3u8MetaInfoParserTask()
//                .setId("panghuys")
//                .setSourcePageUrl("https://www.panghuys.com/ph/144136-1-1.html")
//                .setUrl("https://api.xdys.vip/webapi/video.php?r=f59cddb1ce5403a19f26ee1150e9ebda.m3u8")
////                .setUrl("")
//        M3u8MetaInfoParserTask parseMetaInfoTask = new M3u8MetaInfoParserTask()
//                .setId("xiaoya_bieBiWoDongShou")
//                .setSourcePageUrl("https://xiaoyakankan.com/post/4996f7842b.html?vod=25_52834-0")
//                .setUrl("https://hnzy.bfvvs.com/play/5eVO5lXa/index.m3u8");
//        AssertUtils.isTrue(parseMetaInfoTask.getUrl().contains(".m3u8"), CommonErrorCodeEnum.ILLEGAL_OPERATION);
//        SpiderTaskQueue.putTask(parseMetaInfoTask);

    }

    /**
     * 打散 LimitKey，按照最乱顺序启动，尽快达到最大下载速度，而非spider线程池被一个 limitKey 占满，但同域名不会乱序
     */
    public static List<M3u8MetaInfoParserTask> reOrderTasks(List<M3u8MetaInfoParserTask> taskList) {
        if (CollectionUtils.isEmpty(taskList)) {
            return taskList;
        }

        int total = taskList.size();
        List<M3u8MetaInfoParserTask> tasks = new ArrayList<>(total);
        Map<String, List<M3u8MetaInfoParserTask>> groupByLimitKey = new HashMap<>(total);
        for (M3u8MetaInfoParserTask task : taskList) {
            LimitStrategy limitStrategy = task.getLimitStrategy();
            if (limitStrategy == null || limitStrategy instanceof NoLimit) {
                tasks.add(task);
            } else {
                groupByLimitKey.computeIfAbsent(limitStrategy.getKey(), k -> new ArrayList<>()).add(task);
            }
        }
        // merge
        int groupNum = groupByLimitKey.keySet().size();
        List<Iterator<M3u8MetaInfoParserTask>> iterators = new ArrayList<>(groupNum);
        groupByLimitKey.values().stream().map(List::iterator).forEach(iterators::add);

        for (int x = 0; tasks.size() < total; x++) {
            int index = x % groupNum;
            Iterator<M3u8MetaInfoParserTask> it = iterators.get(index);
            if (it.hasNext()) {
                tasks.add(it.next());
            }
        }
        return tasks;
    }

    private static void compatibleWithOldVersion(M3u8MetaInfoParserTask task) {
    }

    public void scanLocalAndRecoverTask() {
        // 重新拉起本地执行过，但没完成的任务
        String historyDir = m3u8FileNameTemplate.getM3u8StorageDir();
        String taskFileName = m3u8FileNameTemplate.calculateTaskFileName();
        String spiderResultFileName = m3u8FileNameTemplate.getSpiderResultFileName();
        String parsedCacheFileName = m3u8FileNameTemplate.getParsedCacheFileName();
        String tsPartDirName = m3u8FileNameTemplate.getTsPartDirName();
        String mergedResulFileName = m3u8FileNameTemplate.getMergedResulFileName();

        if (!FileUtil.exist(historyDir)) {
            // 无历史任务
            return;
        }
        File[] taskDirArr = new File(historyDir).listFiles();
        if (ArrayUtils.isEmpty(taskDirArr)) {
            // 无历史任务
            return;
        }
        finishedTaskIdToDirMap = new HashMap<>(taskDirArr.length);
        List<M3u8MetaInfoParserTask> taskList = new ArrayList<>(taskDirArr.length);
        List<File> missingUriTaskDirList = new ArrayList<>(taskDirArr.length);
        List<File> unknownDirList = new ArrayList<>(taskDirArr.length);

        // 遍历所有任务目录
        for (int i = 0; i < taskDirArr.length; i++) {
            File taskDir = taskDirArr[i];
            // 【----------跳过非预期目录----------】
            if (!taskDir.isDirectory()) {
                // 忽略非文件夹：本程序都会生成子文件夹的
                continue;
            }
            File[] files = taskDir.listFiles();
            if (ArrayUtils.isEmpty(files)) {
                // 忽略空文件夹: 无法续传，也可能不是本程序生成的
                unknownDirList.add(taskDir);
                continue;
            }

            // 【----------整理为文件名 - File 方便操作----------】
            Map<String, File> nameToFileMap = Arrays.stream(files).collect(Collectors.toMap(File::getName, f -> f));
            File resultFile = nameToFileMap.get(mergedResulFileName);
            if (resultFile != null) {
                // 已经合成了，放入待归集 map
                finishedTaskIdToDirMap.put(taskDir.getName(), taskDir);
                continue;
            }

            // 【----------没下载完的继续下载----------】
            File taskInfoFile = nameToFileMap.get(taskFileName);
            if (taskInfoFile == null) {
                // 无法续传
                missingUriTaskDirList.add(taskDir);
                continue;
            }
            String taskInfo = FileUtil.readString(taskInfoFile, AppInfo.charset());
            M3u8MetaInfoParserTask task = JsonUtils.parseObject(taskInfo, M3u8MetaInfoParserTask.class);
            if (task.getRecoverCount() > CONFIG_RECOVER_MAX) {
                // 重新唤起次数过多，告警，跳过。可能是启动次数过多 or 每次启动都下载失败
                unknownDirList.add(taskDir);
                continue;
            }
            // 兼容代码：resetLimitStrategy
//            String domain = HtmlUtil.parseDomainFromUrl(task.getUrl());
//            task.setLimitStrategy(new FixWindowLimit(HtmlUtil.parseDomainFromUrl(task.getUrl()),
//                    spiderUrlsManager.isLimitRateUse503(domain) ? Duration.ofMillis(2500) : Duration.ofMillis(20)
//            ));
//            if (task.getLimitStrategy() == null || task.getLimitStrategy().getKey().contains("\\")) {
//                task.setLimitStrategy(new FixWindowLimit(HtmlUtil.parseDomainFromUrl(task.getUrl()),
//                        spiderUrlsManager.isLimitRateUse503(domain) ? Duration.ofMillis(2500) : Duration.ofMillis(20)
//                ));
//            }

            File spiderResultFile = nameToFileMap.get(spiderResultFileName);
            // 兼容代码：设置 idempotentFilePath 字段
            if (task.getIdempotentFilePath() == null && spiderResultFile != null && spiderResultFile.exists()) {
                task.setIdempotentFilePath(spiderResultFile.getAbsolutePath());
            }

            task.setRecoverCount(task.getRecoverCount() + 1);
            File parsedCacheFile = nameToFileMap.get(parsedCacheFileName);
            if (parsedCacheFile != null) {
                String cacheFileStr = FileUtil.readString(parsedCacheFile, AppInfo.charset());
                M3u8MetaInfo meta = JsonUtils.parseObject(cacheFileStr, M3u8MetaInfo.class);
                task.setCachedM3u8MetaInfo(meta);
            } // fixme 历史数据 完整性校验
            taskList.add(task);
            log.debug("recover task {} recoverCount={} dir={}", task.getId(), task.getRecoverCount(), taskDir.getAbsolutePath());

            //File spiderResultFile = nameToFileMap.get(spiderResultFileName);
            //File parsedCacheFile = nameToFileMap.get(parsedCacheFileName);
        }
        int totalCanRestartCount = taskList.size();
        try {
            if (deleteTaskCanNotRetry) {
                // 删除这些无法重试的文件
                for (File file : missingUriTaskDirList) {
                    FileUtils.forceDelete(file);
                }
            }
            // 只拿进度比较快的前十个重试
            taskList.sort((task1, task2) -> Long.compare(
                    FileUtils.sizeOfDirectory(new File(task2.getIdempotentFilePath().replace("\\" + m3u8FileNameTemplate.getSpiderResultFileName(), ""))),
                    FileUtils.sizeOfDirectory(new File(task1.getIdempotentFilePath().replace("\\" + m3u8FileNameTemplate.getSpiderResultFileName(), "")))
                    )
            );
            taskList = ListUtil.sub(taskList, 0, 10);

            // 删除这些无法重试 && 没什么进度的文件
            if (deleteTaskNoProcess) {
                for (File dir : unknownDirList) {
                    if (FileUtils.sizeOfDirectory(dir) < CONFIG_DELETE_THRESHOLD) {
                        log.info("Delete cant retry dir dir={}, Size={}M", dir.getAbsolutePath(), FileUtils.sizeOfDirectory(dir) / 1024 / 1024);
                        FileUtils.forceDelete(dir);
                    } else {
                        log.info("Not Retryable Dir={}, size={}M, Remain.", dir.getAbsolutePath(), FileUtils.sizeOfDirectory(dir) / 1024 / 1024);
                    }
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        log.info("recover total={}, restart={} finished={}", totalCanRestartCount, taskList.size(), finishedTaskIdToDirMap.size());
//        for (M3u8MetaInfoParserTask t : taskList) {
//            String fs  = t.getIdempotentFilePath();
//            File dir = new File(fs).getParentFile();
//            FileUtil.del(dir);
//        }
        // 【==============启动可继续的任务 ==============按照最乱顺序启动（按照域名分类，然后依次尝试，有利于忽略域名级限速，最大化利用网络带宽）
        reOrderTasks(taskList).forEach(SpiderTaskQueue::putTask);
        //moveAllResults in async
        if (!MOVE_FINISHED) {
            return;
        }
        Threads.execute(() -> {
                    long moveMb = 0;
                    int deleteCount = 0;
                    long deleteMb = 0;
                    List<File> toCopyFileList = new ArrayList<>(finishedTaskIdToDirMap.size());
                    for (File dir : finishedTaskIdToDirMap.values()) {
                        String desFile = CONFIG_RESULT_COLLECT_DIR + dir.getName() + ".ts";
                        String sourceFile = dir.getAbsolutePath() + File.separator + m3u8FileNameTemplate.getMergedResulFileName();
                        boolean copied = PathUtil.fastCompare(sourceFile, desFile);
                        if (!copied) {
                            toCopyFileList.add(dir);
                            moveMb += new File(sourceFile).length() / 1024 / 1024;
                        } else {
                            // 删除这些完成且收集完毕的，释放磁盘空间
                            try {
                                deleteCount++;
                                deleteMb += FileUtils.sizeOfDirectory(dir) / 1024 / 1024;
                                log.info("Delete already moved file={}, Size={}M", dir.getAbsolutePath(), deleteMb);
                                FileUtils.forceDelete(dir);
                                Thread.sleep(300);
                            } catch (Exception e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                    log.info("ToMoveNum={}, Size={}G ; DeletedNum={}, Size={}Mb", toCopyFileList.size(), moveMb / 1024, deleteCount, deleteMb);

                    for (File f : toCopyFileList) {
                        String desFile = CONFIG_RESULT_COLLECT_DIR + f.getName() + ".ts";

                        PathUtil.copyOverrideIfNotEqual(f.getAbsolutePath() + File.separator + m3u8FileNameTemplate.getMergedResulFileName(), desFile);

                        // 复制时候不要连续复制，避免硬盘过热
                        // 因为若下载和归集目录在同一个盘会出现同时读写，导致读写性能都受到较大影响，
                        // sleep 反而会充分利用硬盘缓冲区，使得整体流程更快，且避免硬盘卡顿，保护硬盘
                        ThreadUtil.sleep(5000);
                        log.info("move [{}], Size={}M", f.getAbsolutePath(), f.length() / 1024 / 1024);

                        // 记录move记录
//                        String logStr = " moved resultFile to " + desFile + "\n";
//                        File logFile = new File(f.getAbsolutePath(), "op.log");
//                        String dateTimeStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
//                        FileUtil.appendLines(Collections.singleton(dateTimeStr + logStr), logFile, AppInfo.charset());
                    }
                }
        );
    }

    public Map<String, File> getFinishedTaskIdToDirMap() {
        return finishedTaskIdToDirMap;
    }
}
